Skip to content

fix(users): reset email verification on email change (#3825)#3854

Merged
PierreBrisorgueil merged 4 commits into
masterfrom
fix/3825-email-change-reset-verified
Jun 13, 2026
Merged

fix(users): reset email verification on email change (#3825)#3854
PierreBrisorgueil merged 4 commits into
masterfrom
fix/3825-email-change-reset-verified

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

  • What changed: PUT /api/users (self-update) and the admin user-update path now reset emailVerified to false, mint a fresh email-verification token, and re-send the verification email when the email address changes — and the mailer is configured. The recover path (the internal verification writer) is exempt.
  • Why: linkProviderByEmail trusts { email, emailVerified: true }. On a mailer-configured deployment emailVerified is a real ownership proof, so an attacker could change their verified address to a victim's, complete a new OAuth handshake, and be linked/annexed. Resetting emailVerified on email change (then re-verifying) closes that surface.
  • Mailer-off is intentionally exempt — not an oversight: deployments without a mailer auto-verify every account at signup (trust-any-email by design), so emailVerified is not an ownership proof there. Resetting it on email change would (a) permanently un-verify the user with no re-verify path (no mailer), breaking OAuth linking, and (b) not close any surface anyway — an attacker would auto-verify the victim address directly at signup, never via a change. So the reset is correctly gated on mailer.isConfigured().
  • Related issues: Closes 🔒 PUT /api/users email change doesn't reset emailVerified → OAuth account-annexation risk #3825

Scope

  • Module(s) impacted: modules/users (service + integration/unit tests)
  • Cross-module impact: none
  • Risk level: low — additive guard; fires only on an actual email-address change on a mailer-configured deployment

Validation

  • npm run lint
  • CI — users.email.change integration suite green; full integration + e2e green

Test plan

New suite: modules/users/tests/users.email.change.integration.tests.js

Scenario Assertion
Mailer-on: email changed emailVerifiedfalse; fresh emailVerificationToken minted; verify-email re-sent
No-change: same email re-submitted emailVerified unchanged, no token, no mail
Same-email: normalised case variant emailVerified unchanged (idempotent)
Mailer-off: email changed emailVerified stays true — intentional exemption (see Why)
Admin path: email changed via admin update Same reset + re-send as self-update
End-to-end re-verify After reset, user follows the new token → emailVerified returns to true
Recover path: email in a recover update Does not reset emailVerified or send mail (verifyEmail invariant)
Mail-send rejects Update still succeeds (fire-and-forget .catch)

Guardrails check

  • No secrets/credentials introduced
  • No risky rename/move of core stack paths
  • Public-OSS clean (no downstream names/domains/infra/board-ids)
  • Tests added when behavior changed

Notes for reviewers

  • Security: closes the OAuth account-annexation surface on mailer-configured deployments; the mailer-off exemption is the documented trade-off above (resetting there is both harmful and ineffective).
  • Reviewer (CodeRabbit fallback): CodeRabbit was Free-plan/rate-limited; an independent cold /critical-review returned OK-with-nits (0 critical/high). Copilot threads resolved.
  • Follow-ups (optional, noted not blocking): extract the 24 h TTL constant; unknown-option else-arm logger.warn.

Changing the account email via the self or admin update paths kept
emailVerified:true on the brand-new address, letting the OAuth
link-by-email flow ({ email, emailVerified: true }) attach a provider
identity to an address the account never proved it owns. On email
change (mailer configured), reset emailVerified, mint a fresh
verification token (24h expiry, same expressions as signup) and
fire-and-forget a verify-email mail to the new address. The recover
path (internal verification writer) and mailer-less deployments
(signup auto-verifies by design) are exempt. Re-sends are covered by
the existing resend-verification endpoint.

refs #3825
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@PierreBrisorgueil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 15 minutes and 35 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more credits in the billing tab to continue.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bbb542df-72ff-49e1-9bc1-55e56a35b63f

📥 Commits

Reviewing files that changed from the base of the PR and between 755af6e and 7b9e45b.

📒 Files selected for processing (3)
  • modules/users/services/users.service.js
  • modules/users/tests/users.email.change.integration.tests.js
  • modules/users/tests/users.service.count.unit.tests.js
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3825-email-change-reset-verified

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 91.98%. Comparing base (755af6e) to head (7b9e45b).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3854      +/-   ##
==========================================
+ Coverage   91.96%   91.98%   +0.02%     
==========================================
  Files         160      160              
  Lines        5289     5303      +14     
  Branches     1698     1702       +4     
==========================================
+ Hits         4864     4878      +14     
  Misses        337      337              
  Partials       88       88              
Flag Coverage Δ
integration 59.94% <100.00%> (+0.10%) ⬆️
unit 72.50% <7.14%> (-0.18%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 755af6e...7b9e45b. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

users.service.js now imports logger (+ mailer + getBaseUrl) for the
email-change reset path. The existing count unit test uses a minimal
config mock (no `log` key); logger.js calls setupFileLogger() at
module-evaluation time, which crashed on config.log.fileLogger.

Add jest.unstable_mockModule stubs for the three new deps so the
minimal-config test keeps running in isolation.
@PierreBrisorgueil PierreBrisorgueil marked this pull request as ready for review June 13, 2026 07:12
Copilot AI review requested due to automatic review settings June 13, 2026 07:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses #3825 by invalidating a user’s verified-email state when their email address changes, preventing OAuth linkProviderByEmail from linking identities to an email the account no longer controls.

Changes:

  • Update UserService.update() to detect email changes, reset emailVerified, mint a new verification token/expiry, and (best-effort) send a re-verification email.
  • Add a new integration test suite covering self-update and admin-update email-change behavior and the OAuth linking guard.
  • Adjust an existing unit test to mock newly introduced dependencies (logger/base URL/mailer) during module import.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
modules/users/services/users.service.js Adds email-change detection + verification reset/token minting + best-effort verification email send.
modules/users/tests/users.email.change.integration.tests.js New integration coverage for email-change behavior across mailer-on/off and admin/self paths.
modules/users/tests/users.service.count.unit.tests.js Adds mocks to keep unit tests stable after new service-level imports.

Comment thread modules/users/services/users.service.js Outdated
Comment thread modules/users/services/users.service.js
Comment thread modules/users/tests/users.email.change.integration.tests.js
Pin that the recover update path never resets emailVerified or sends

a re-verification mail (verifyEmail invariant); complete the count

unit-test mailer mock with isConfigured. refs #3825
Adds a JSDoc for the helper (nullability) and the deliberate duplication

of the repository-layer normalization. Addresses review on #3854. refs #3825
@PierreBrisorgueil PierreBrisorgueil merged commit 137f37e into master Jun 13, 2026
8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/3825-email-change-reset-verified branch June 13, 2026 12:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🔒 PUT /api/users email change doesn't reset emailVerified → OAuth account-annexation risk

2 participants